<template>
  <mew-module
    class="d-flex flex-grow-1 pt-6"
    title="Interact with contract"
    :has-elevation="true"
    :has-indicator="true"
  >
    <template #moduleBody>
      <div>
        <mew-select
          v-model="currentContract"
          :items="mergedContracts"
          label="Contract Name"
          class="ContractSelect"
          normal-dropdown
          @input="selectedContract"
        />
        <mew-input
          v-model="contractAddress"
          label="Contract Address"
          placeholder=" "
          class="mr-3 flex-grow-1 full-width"
          :persistent-hint="nametag.length > 0"
          :hint="nametag"
        />

        <v-textarea
          v-model="abi"
          no-resize
          outlined
          name="input-7-4"
          label="ABI/JSON Interface"
        ></v-textarea>

        <div class="text-right">
          <mew-button
            title="Clear all"
            :has-full-width="false"
            btn-style="light"
            class="mr-4"
            @click.native="resetDefaults"
          />
          <mew-button
            title="Interact"
            class="InteractButton"
            :disabled="!canInteract"
            :has-full-width="false"
            @click.native="showInteract"
          />
        </div>
      </div>

      <!--
      ============================================================================
      Overlay
      ============================================================================
      -->
      <mew-overlay
        :footer="{
          text: 'Need help?',
          linkTitle: 'Contact support',
          link: 'mailto:support@myetherwallet.com'
        }"
        title="Interact with contract"
        :show-overlay="interact"
        :close="closeInteract"
        content-size="medium"
      >
        <mew-select
          label="Function"
          :items="methods"
          class="mt-4 mt-lg-0 mb-1 FunctionSelect"
          normal-dropdown
          @input="methodSelect"
        />

        <div v-show="selectedMethod.inputs.length" class="mew-heading-2 mb-3">
          Inputs
        </div>
        <div
          v-for="(input, idx) in selectedMethod.inputs"
          :key="input.name + idx"
          class="input-item-container"
        >
          <mew-input
            v-if="getType(input.type).type !== 'radio'"
            :label="`${input.name} (${input.type})`"
            :rules="[
              value => {
                return isValidInput(value, getType(input.type).solidityType);
              }
            ]"
            @input="valueInput(idx, $event)"
          />
          <div
            v-if="getType(input.type).type === 'radio'"
            class="bool-input-container"
          >
            <div class="bool-items">
              <mew-checkbox
                v-model="input.value"
                :label="input.name"
                type="radio"
                checked
                @input="valueInput(idx, $event)"
              />
            </div>
          </div>
        </div>
        <div>
          <mew-input
            v-if="isPayableFunction"
            label="ETH amount:"
            :rules="[
              value => {
                return hasEnough ? '' : 'Not enough ETH';
              }
            ]"
            type="number"
            @input="payableInput($event)"
          />
        </div>
        <div class="text-center mt-2">
          <mew-button
            :title="isViewFunction ? 'Call' : 'Write'"
            :has-full-width="false"
            btn-size="xlarge"
            class="CallFunctionButton"
            :disabled="canProceed"
            @click.native="readWrite"
          />
        </div>

        <v-divider v-if="hasOutputs" class="mt-9 mb-8" />

        <div v-if="hasOutputs" style="display: contents">
          <div class="mew-heading-2">Results</div>
          <div
            v-for="(output, idx) in selectedMethod.outputs"
            :key="output.name + idx"
            class="d-flex align-center justify-space-between my-4"
          >
            <div class="text-capitalize mr-2">
              {{ output.name !== '' ? output.name : selectedMethod.name }}
            </div>
            <div class="font-weight-medium">{{ output.value }}</div>
          </div>
        </div>
      </mew-overlay>
    </template>
  </mew-module>
</template>

<script>
import Vue from 'vue';
import { mapState, mapGetters } from 'vuex';
import { toBN, toWei } from 'web3-utils';
import { isString, throttle } from 'lodash';
import { getAddressInfo } from '@kleros/address-tags-sdk';

import { isAddress } from '@/core/helpers/addressUtils';
import { stringToArray } from '@/core/helpers/common';
import {
  parseJSON,
  parseABI,
  getType as getInputType,
  isContractArgValid
} from './handlers/common';
import { ERROR, Toast } from '../toast/handler/handlerToast';
import handlerAnalyticsMixin from '../analytics-opt-in/handlers/handlerAnalytics.mixin';
import { CONTRACT } from '../analytics-opt-in/handlers/configs/events';

export default {
  name: 'ModuleContractInteract',
  mixins: [handlerAnalyticsMixin],
  data() {
    return {
      currentContract: null,
      interact: false,
      inputsValid: false,
      hasEnough: false,
      abi: [],
      contractAddress: '',
      selectedMethod: {
        inputs: [],
        outputs: []
      },
      outputValues: [],
      ethPayable: '0',
      nametag: '',
      networkContracts: []
    };
  },
  computed: {
    ...mapState('wallet', ['address', 'web3', 'balance']),
    ...mapGetters('global', ['network', 'gasPrice', 'localContracts']),
    canProceed() {
      if (this.isPayableFunction) {
        if (!this.canPay) {
          return true;
        }
        return (
          !this.inputsValid &&
          !!this.selectedMethod.inputs.length &&
          this.canPay
        );
      }
      return !this.inputsValid && !!this.selectedMethod.inputs.length;
    },
    isViewFunction() {
      return (
        this.selectedMethod.constant ||
        this.selectedMethod.stateMutability === 'view'
      );
    },
    isPayableFunction() {
      return this.selectedMethod.stateMutability === 'payable';
    },
    canPay() {
      if (this.isPayableFunction) {
        return this.hasEnough;
      }
      return true;
    },
    mergedContracts() {
      const checkContract = arr =>
        arr.filter(contract => isString(contract.name));
      return [
        { text: 'Select a Contract', selectLabel: true, divider: true }
      ].concat(
        checkContract(this.localContracts),
        checkContract(this.networkContracts)
      );
    },
    methods() {
      if (this.canInteract) {
        return JSON.parse(this.abi).filter(item => {
          if (
            item.type !== 'constructor' &&
            item.type !== 'event' &&
            item.type !== 'fallback'
          ) {
            return item;
          }
        });
      }
      return [];
    },
    canInteract() {
      return isAddress(this.contractAddress) && parseABI(parseJSON(this.abi));
    },
    hasOutputs() {
      const outputsWithValues = this.selectedMethod.outputs.filter(item => {
        if (item.value !== '') {
          return item;
        }
      });

      return outputsWithValues.length > 0;
    }
  },
  watch: {
    contractAddress(newVal) {
      this.nametag = '';
      if (!newVal) {
        this.contractAddress = '';
      }
      if (newVal && isAddress(newVal.toLowerCase())) {
        this.resolveAddress();
      }
    },
    web3: {
      handler: function () {
        this.generateNetworkContracts();
      }
    }
  },
  mounted() {
    this.generateNetworkContracts();
  },
  methods: {
    generateNetworkContracts() {
      this.network.type.contracts.then(contracts => {
        this.networkContracts = contracts;
      });
    },
    resetDefaults() {
      this.currentContract = null;
      this.abi = [];
      this.contractAddress = '';
      this.interact = false;
      this.selectedMethod = {
        inputs: [],
        outputs: []
      };
    },
    readWrite() {
      const params = [];
      for (const _input of this.selectedMethod.inputs) {
        if (_input.type.includes('[]')) {
          if (_input.value === '[]') {
            params.push([]);
          } else {
            params.push(stringToArray(_input.value));
          }
        } else {
          params.push(_input.value);
        }
      }
      const caller = this.currentContract.methods[
        this.selectedMethod.name
      ].apply(this, params);
      if (this.isViewFunction) {
        this.trackContract(CONTRACT.INTERACT_W_CONTRACT_READ);
        caller
          .call()
          .then(result => {
            this.trackContract(CONTRACT.INTERACT_W_CONTRACT_READ_SUCCESS);
            if (this.selectedMethod.outputs.length === 1) {
              this.selectedMethod.outputs[0].value = result;
              Vue.set(
                this.selectedMethod.outputs,
                0,
                this.selectedMethod.outputs[0]
              );
            } else if (this.selectedMethod.outputs.length > 1) {
              this.selectedMethod.outputs.forEach((out, idx) => {
                out.value = result[idx];
                Vue.set(this.selectedMethod.outputs, idx, out);
              });
            }
          })
          .catch(({ message }) => {
            this.trackContract(CONTRACT.INTERACT_W_CONTRACT_READ_FAIL);
            Toast(message, {}, ERROR);
          });
      } else if (this.isPayableFunction) {
        this.trackContract(CONTRACT.INTERACT_W_CONTRACT_WRITE);
        const rawTx = {
          to: this.contractAddress,
          from: this.address,
          value: this.ethPayable,
          data: caller.encodeABI()
        };

        this.web3.eth
          .estimateGas(rawTx)
          .then(gasLimit => {
            this.trackContract(CONTRACT.INTERACT_W_CONTRACT_WRITE_SUCCESS);
            rawTx.gas = gasLimit;
            caller.send(rawTx);
          })
          .catch(({ message }) => {
            this.trackContract(CONTRACT.INTERACT_W_CONTRACT_WRITE_FAIL);
            Toast(message, {}, ERROR);
          });
      } else {
        this.trackContract(CONTRACT.INTERACT_W_CONTRACT_WRITE);
        caller
          .send({ from: this.address })
          .then(() => {
            this.trackContract(CONTRACT.INTERACT_W_CONTRACT_WRITE_SUCCESS);
          })
          .catch(({ message }) => {
            this.trackContract(CONTRACT.INTERACT_W_CONTRACT_WRITE_FAIL);
            Toast(message, {}, ERROR);
          });
      }
    },
    payableInput(amount) {
      if (!amount || amount === '') amount = '0';
      this.ethPayable = toWei(amount, 'ether');
      this.hasEnough = toBN(this.ethPayable).lte(this.balance);
    },
    valueInput(idx, value) {
      this.selectedMethod.inputs[idx].value = value;
      this.inputsValid = true;
      for (const _input of this.selectedMethod.inputs) {
        if (
          !this.isValidInput(
            _input.value,
            this.getType(_input.type).solidityType
          )
        )
          this.inputsValid = false;
      }
    },
    selectedContract(selected) {
      if (parseABI(parseJSON(selected.abi))) {
        if (typeof selected.abi !== 'string')
          this.abi = JSON.stringify(selected.abi);
        else this.abi = selected.abi;
      }
      if (isAddress(selected.address)) {
        this.contractAddress = selected.address;
      }
    },
    closeInteract() {
      this.interact = false;
      this.resetDefaults();
    },
    showInteract() {
      this.interact = true;
      this.currentContract = new this.web3.eth.Contract(
        JSON.parse(this.abi),
        this.contractAddress
      );
    },
    methodSelect(evt) {
      if (evt && evt.inputs && evt.outputs) {
        this.inputsValid = false;
        this.selectedMethod = evt;
        this.selectedMethod.inputs.forEach(v => (v.value = ''));
        this.selectedMethod.outputs.forEach(v => (v.value = ''));
        this.outputValues = [];
      }
    },

    isValidInput(value, sType) {
      return isContractArgValid(value, sType);
    },
    getType(type) {
      return getInputType(type);
    },
    /**
     * Resolves address and @returns name
     */
    resolveAddress: throttle(async function () {
      try {
        await getAddressInfo(
          this.contractAddress,
          'https://ipfs.kleros.io'
        ).then(data => {
          this.nametag = data?.publicNameTag || '';
        });
      } catch (e) {
        this.nametag = '';
      }
    }, 300)
  }
};
</script>
